/*
 * ﻿Copyright (C) 2013-2014 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.eatestutil.ejbtestutil;

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import javax.inject.Inject;
import javax.naming.NamingException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.newmainsoftech.eatestutil.container.AbstractEjbContainerMateImpl;
import com.newmainsoftech.eatestutil.container.EjbContainerMate;
import com.newmainsoftech.eatestutil.container.EjbContextCloseHandler;
import com.newmainsoftech.injection.Swap;

/**
 * Abstract test helper class to assist test involving EJB entity. <br />
 * 
 * @author Arata Y.
 */
public abstract class AbstractTestBaseInvolvingEntity implements EntityManagerFacadeProvider {
	private Logger logger = LoggerFactory.getLogger( this.getClass());
		protected Logger getLogger() {
			return logger;
		}
	@Inject @Swap( isOnlyForNull = true) // These annotations are just as reminder not to forget restate in subclass. 
	private AbstractEjbContainerMateImpl ejbContainerMate;
		protected void setEjbContainerMate( final AbstractEjbContainerMateImpl ejbContainerMate) {
			this.ejbContainerMate = ejbContainerMate;
		}
		public EjbContainerMate getEjbContainerMate() {
			return ejbContainerMate;
		}
		public EjbContextCloseHandler getEjbContextCloseHandler() {
			return ejbContainerMate;
		}
		
	private Class<? extends EntityManagerFacade> entityManagerFacadeImplClass;
		public Class<? extends EntityManagerFacade> getEntityManagerFacadeImplClass() {
			return entityManagerFacadeImplClass;
		}
		public void setEntityManagerFacadeImplClass( 
				final Class<? extends EntityManagerFacade> entityManagerFacadeImplClass) {
			if ( entityManagerFacadeImplClass.isInterface()) {
				throw new IllegalArgumentException( 
						String.format(
								"%1$s is interface class not implementation class of %2$s interface.",
								entityManagerFacadeImplClass.getSimpleName(),
								EntityManagerFacade.class.getSimpleName())
						);
			}
			this.entityManagerFacadeImplClass = entityManagerFacadeImplClass;
		}
	
	private EntityManagerFacade entityManagerFacade;
		private final ReentrantLock EntityManagerFacadeLock = new ReentrantLock();
			protected ReentrantLock getEntityManagerFacadeLock() {
				return EntityManagerFacadeLock;
			}
			public final static long EntityManagerFacadeLockAcquireTimeOutDefaultValue = 250;
				public static long getEntityManagerFacadeLockAcquireTimeOutDefaultValue() {
					return EntityManagerFacadeLockAcquireTimeOutDefaultValue;
				}
			private long entityManagerFacadeLockAcquireTimeOut 
			= AbstractTestBaseInvolvingEntity.getEntityManagerFacadeLockAcquireTimeOutDefaultValue();
				public long getEntityManagerFacadeLockAcquireTimeOut() {
					return entityManagerFacadeLockAcquireTimeOut;
				}
				public void setEntityManagerFacadeLockAcquireTimeOut(
						long entityManagerFacadeLockAcquireTimeOut) {
					this.entityManagerFacadeLockAcquireTimeOut = entityManagerFacadeLockAcquireTimeOut;
				}
		public void setEntityManagerFacade( final EntityManagerFacade entityManagerFacade) {
			this.entityManagerFacade = entityManagerFacade;
		}
		protected EntityManagerFacade lookupEntityManagerFacade() {
			EntityManagerFacade entityManagerFacadeObj = null;
				final Class<?> entityManagerFacadeImplClass = getEntityManagerFacadeImplClass();
				if ( entityManagerFacadeImplClass == null) {
					try {
						entityManagerFacadeObj
						= getEjbContainerMate().contextLookup( EntityManagerFacade.class);
					} 
					catch( NamingException exception) {
						throw new RuntimeException(
								String.format(
										"Failure in finding %1$s instance out of EJB container. " 
										+ "Cause: %1$s.",
										EntityManagerFacade.class.getSimpleName(), 
										exception.toString( true)),
								exception);
					}
				}
				else {
					try {
						entityManagerFacadeObj
						= getEjbContainerMate().contextLookup( 
								entityManagerFacadeImplClass, EntityManagerFacade.class);
					} 
					catch( NamingException exception) {
						throw new RuntimeException(
								String.format(
										"Failure in finding %1$s instance out of EJB container. " 
										+ "Cause: %1$s.",
										entityManagerFacadeImplClass.getSimpleName(), 
										exception.toString( true)),
								exception);
					}
				}
			return entityManagerFacadeObj;
		}
		public EntityManagerFacade getEntityManagerFacade() {
			if ( entityManagerFacade != null) return entityManagerFacade;
			final EntityManagerFacade entityManagerFacadeObj = lookupEntityManagerFacade();
			setEntityManagerFacade( entityManagerFacadeObj);
			return entityManagerFacadeObj;
		}
	
	protected Method locateMethod( final String methodName, final Class<?>[] parameterTypes) 
	throws Exception {
		Method method = null;
			Class<?>[] parameterTypesCopy = parameterTypes;
				if ( parameterTypesCopy == null) {
					parameterTypesCopy = new Class<?>[]{};
				}
			for( 	Class<?> classObj = this.getClass(); 
					classObj != null; 
					classObj = classObj.getSuperclass()) {
				try {
					method = classObj.getDeclaredMethod( methodName, parameterTypesCopy);
					break;
				} 
				catch( NoSuchMethodException exception) {
					continue;
				} 
			}
			if ( method == null) {
				throw new NoSuchMethodException(
						MessageFormat.format(
								"Could not locate {0} method with " 
								+ "{1,choice,0#no argument|1#argument type of {2}|1<argument types of {2}}" 
								+ "in {3} class.",
								methodName,
								parameterTypesCopy.length,
								Arrays.toString( parameterTypesCopy),
								this.getClass().getSimpleName())
						);
			}
		return method;
	}
	
	/**
	 * Query entities of given class in datastore<br />
	 * Unless calling this from EJB session/facade bean, this should be called via 
	 * <code>{@link EntityManagerFacade#runExternalMethod(Object, Method, Object...)}</code> method. 
	 * Otherwise you will encounter the next exception: java.lang.IllegalStateException: Attempting to 
	 * execute an operation on a closed EntityManager.
	 * 
	 * @param entityClass
	 * @return List of entities of given class found in datastore.
	 */
	protected <T> List<T> getQueryResultList( final Class<T> entityClass) {
		final EntityManagerFacade entityManagerFacade = getEntityManagerFacade();
		
		final CriteriaBuilder criteriaBuilder = entityManagerFacade.getCriteriaBuilder();
		final CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery( entityClass);
			final Root<T> root = criteriaQuery.from( entityClass);
			criteriaQuery.select( root);
		final TypedQuery<T> typedQuery = entityManagerFacade.createQuery( criteriaQuery);
		return typedQuery.getResultList();
	}
		private Method getQueryResultListMethod;
			protected void setGetQueryResultListMethod( final Method getQueryResultListMethod) {
				this.getQueryResultListMethod = getQueryResultListMethod;
			}
			public Method getGetQueryResultListMethod() throws Exception {
				if ( this.getQueryResultListMethod == null) {
					final String methodName = "getQueryResultList";
					Method method = locateMethod( methodName, new Class<?>[]{ Class.class});
					setGetQueryResultListMethod( method); 
				}
				return getQueryResultListMethod;
			}

	protected void removeEntitiy( final Object entity) {
		final EntityManagerFacade entityManagerFacade = getEntityManagerFacade();
		
		if ( entityManagerFacade.contains( entity)) {
			entityManagerFacade.refresh( entity);
			entityManagerFacade.remove( entity);
		}
		else {
			entityManagerFacade.remove( 
					entityManagerFacade.merge( entity));
		}
		
		final Logger logger = getLogger();
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Removed %1$s entity from datasource.",
								entity.toString())
						);
			}
	}
		private Method removeEntitiyMethod;
			protected void setRemoveEntitiyMethod( final Method removeEntitiyMethod) {
				this.removeEntitiyMethod = removeEntitiyMethod;
			}
			public Method getRemoveEntitiyMethod() throws Exception {
				if ( removeEntitiyMethod == null) {
					final String methodName = "removeEntitiy";
					Method method = locateMethod( methodName, new Class<?>[]{ Object.class});
					setRemoveEntitiyMethod( method);
				}
				return removeEntitiyMethod;
			}

	protected void removeEntitiyViaId( final Class<?> entityClass, final Object key) {
		final EntityManagerFacade entityManagerFacade = getEntityManagerFacade();
		
		Object dsEntity = entityManagerFacade.getReference( entityClass, key);
		entityManagerFacade.remove( dsEntity);
		
		final Logger logger = getLogger();
			if ( logger.isDebugEnabled()) {
				logger.debug( 
						String.format(
								"Removed %1$s entity from datasource.",
								dsEntity)
						);
			}
	}
	
		private Method removeEntitiyViaIdMethod;
			protected void setRemoveEntitiyViaIdMethod( final Method removeEntitiyViaIdMethod) {
				this.removeEntitiyViaIdMethod = removeEntitiyViaIdMethod;
			}
			public Method getRemoveEntitiyViaIdMethod() throws Exception {
				if ( removeEntitiyViaIdMethod == null) {
					final String methodName = "removeEntitiyViaId";
					Method method = locateMethod( methodName, new Class<?>[]{ Class.class, Object.class});
					setRemoveEntitiyViaIdMethod( method);
				}
				return removeEntitiyViaIdMethod;
			}
}
